﻿/*
Copyright (c) 2013, Douglas Stebila, Brendon Park, David Sweeney, Nathan Wills, Simon Lockhart, Thomas Phan
All rights reserved.

Redistribution and use in source and binary forms, with or without modification,
are permitted provided that the following conditions are met:

* Redistributions of source code must retain the above copyright notice, this
  list of conditions and the following disclaimer.

* Redistributions in binary form must reproduce the above copyright notice, this
  list of conditions and the following disclaimer in the documentation and/or
  other materials provided with the distribution.

THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR
ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
(INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON
ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
*/
using System;
using System.Linq;
using System.Security.Cryptography;

namespace NMLE
{
    /// <summary>
    ///     Base class for MLE transforms
    /// </summary>
    /// <typeparam name="THashAlgorithm">
    ///     The hash algorythm used to derive the TransformKey
    /// </typeparam>
    /// <typeparam name="TEncryptionAlgorithm">
    ///      The encryption algorythm used transform the message
    /// </typeparam>
    public abstract class MLETransformBase<THashAlgorithm, TEncryptionAlgorithm>
        : ICryptoTransform
        where THashAlgorithm : HashAlgorithm, new()
        where TEncryptionAlgorithm : SymmetricAlgorithm, new()
    {
        private byte[] _tag;

        /// <summary>
        ///     When called from a inheriting class instantiates the Hash
        ///     and encryption algorytms.
        /// </summary>
        /// <param name="transformKey">The TransformKey used to transform the message</param>
        /// <param name="direction">
        ///     The direction of the cryptographic algorytm
        /// </param>
        protected MLETransformBase(Byte[] transformKey, EncryptionDirection direction)
        {
            Direction = direction;
            TransformKey = transformKey;
            UnderlyingHashTransform = new THashAlgorithm();
            UnderlyingSymetricTransform = GetTransform(TransformKey, Direction);
        }

        /// <summary>
        ///     Performs application-defined tasks associated with freeing,
        ///     releasing, or resetting unmanaged resources.
        /// </summary>
        /// <filterpriority>2</filterpriority>
        public void Dispose()
        {
            UnderlyingHashTransform.Dispose();
            UnderlyingSymetricTransform.Dispose();
        }

        /// <summary>
        ///     Transforms the specified region of the input byte array and 
        ///     copies the resulting transform to the specified region of the 
        ///     output byte array.
        /// </summary>
        /// <returns>
        ///     The number of bytes written.
        /// </returns>
        /// <param name="inputBuffer">
        ///     The input for which to compute the transform. 
        /// </param>
        /// <param name="inputOffset">
        ///     The offset into the input byte array from which to begin using 
        ///     data. 
        /// </param>
        /// <param name="inputCount">
        ///     The number of bytes in the input byte array to use as data. 
        /// </param>
        /// <param name="outputBuffer">
        ///     The output to which to write the transform. 
        /// </param>
        /// <param name="outputOffset">
        ///     The offset into the output byte array from which to begin 
        ///     writing data. 
        /// </param>
        public abstract int TransformBlock(
            byte[] inputBuffer, 
            int inputOffset, 
            int inputCount,
            byte[] outputBuffer, 
            int outputOffset);

        /// <summary>
        ///     Transforms the specified region of the specified byte array.
        /// </summary>
        /// <returns>
        ///     The computed transform.
        /// </returns>
        /// <param name="inputBuffer">
        ///     The input for which to compute the transform. 
        /// </param>
        /// <param name="inputOffset">
        ///     The offset into the byte array from which to begin using data. 
        /// </param>
        /// <param name="inputCount">
        ///     The number of bytes in the byte array to use as data. 
        /// </param>
        public abstract byte[] TransformFinalBlock(
            byte[] inputBuffer, 
            int inputOffset, 
            int inputCount
            );

        /// <summary>
        ///     Gets the input block size.
        /// </summary>
        /// <returns>
        ///     The size of the input data blocks in bytes.
        /// </returns>
        public int InputBlockSize
        {
            get { return UnderlyingSymetricTransform.InputBlockSize; }
        }

        /// <summary>
        ///     Gets the output block size.
        /// </summary>
        /// <returns>
        ///     The size of the output data blocks in bytes.
        /// </returns>
        public int OutputBlockSize
        {
            get { return UnderlyingSymetricTransform.OutputBlockSize; }
        }

        /// <summary>
        ///     Gets a value indicating whether multiple blocks can be
        ///     transformed.
        /// </summary>
        /// <returns>
        ///     true if multiple blocks can be transformed; otherwise, false.
        /// </returns>
        public bool CanTransformMultipleBlocks
        {
            get
            {
                return UnderlyingSymetricTransform.CanTransformMultipleBlocks
                       && UnderlyingHashTransform.CanTransformMultipleBlocks;
            }
        }

        /// <summary>
        ///     Gets a value indicating whether the current transform can be
        ///     reused.
        /// </summary>
        /// <returns>
        ///     true if the current transform can be reused; otherwise, false.
        /// </returns>
        public bool CanReuseTransform
        {
            get
            {
                return UnderlyingSymetricTransform.CanReuseTransform
                       && UnderlyingHashTransform.CanReuseTransform;
            }
        }

        /// <summary>
        ///     The MLE tag used to uniquely identify the output of this 
        ///     transform
        /// </summary>
        public Byte[] Tag
        {
            get
            {
                if (_tag != null)
                    return _tag;
                throw new InvalidOperationException(
                    "Cannot access tag until implemenenting algorytm reports a " +
                    "TransformKey check"
                    );
            }
            set
            {
                _tag = value;
            }
        }

        /// <summary>
        ///     The TransformKey used to transform the message
        /// </summary>
        protected Byte[] TransformKey { get; private set; }

        /// <summary>
        ///     Gets the Key of the algorytm
        /// </summary>
        public abstract Byte[] Key { get; }

        /// <summary>
        ///     The direction of the cryptographic algorytm
        /// </summary>
        protected EncryptionDirection Direction { get; private set; }

        /// <summary>
        ///     the hashing algorythm that was used to derive they TransformKey
        /// </summary>
        protected HashAlgorithm UnderlyingHashTransform
        {
            get; 
            private set;
        }

        /// <summary>
        ///     The symetric algorythm used to encrypt the message
        /// </summary>
        protected ICryptoTransform UnderlyingSymetricTransform
        {
            get; 
            private set;
        }

        protected void ReportHashCheck(byte[] hash)
        {
            if (!IsValidKeyForMessage(hash))
                throw new UnmatchedKeyException(
                    "The Key used to encrypt the message does not match the " +
                    "message is was used to encrypt."
                    );
            _tag = new Rfc2898DeriveBytes(hash,TransformKey,1000)
                .GetBytes(hash.Length);
        }

        /// <summary>
        ///     Returns a ICryptoTransform for TEncryptionAlgorythm for the
        ///     given TransformKey and direction
        /// </summary>
        /// <param name="key">
        ///     the TransformKey used to encrypt or decrypt the message
        /// </param>
        /// <param name="direction">
        ///     The direction of encryption
        /// </param>
        /// <returns></returns>
        protected static ICryptoTransform GetTransform(
            byte[] key,
            EncryptionDirection direction
            )
        {
            var symetricAlgorythm = new TEncryptionAlgorithm
            {
                BlockSize = key.Length *8,
                KeySize = key.Length * 8,
                Key = key,
                IV = key
            };

            switch (direction)
            {
                case EncryptionDirection.Decrypt:
                    return symetricAlgorythm.CreateDecryptor();
                case EncryptionDirection.Encrypt:
                    return symetricAlgorythm.CreateEncryptor();
                default:
                    throw new ArgumentException(
                        "Direction must be either " +
                        "EncryptionDirection.Decrypt or " +
                        "EncryptionDirection.Encrypt"
                        );
            }
        }

        private Boolean IsValidKeyForMessage(byte[] hash)
        {
            var checkKey = new byte[Key.Length];
            Array.Copy(hash, checkKey, checkKey.Length);
            return Key.SequenceEqual(checkKey);
        }

    }
}
